package com.ruben.simplestreamquery.util;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.ruben.simplestreamquery.pojo.bo.AttachBO;
import com.ruben.simplestreamquery.pojo.bo.BaseDbBO;
import com.ruben.simplestreamquery.pojo.bo.RelationBO;
import com.ruben.simplestreamquery.pojo.bo.SubBO;
import io.github.vampireachao.stream.core.bean.BeanHelper;
import io.github.vampireachao.stream.core.lambda.LambdaExecutable;
import io.github.vampireachao.stream.core.lambda.LambdaHelper;
import io.github.vampireachao.stream.core.lambda.function.SerSupp;
import io.github.vampireachao.stream.core.reflect.ReflectHelper;
import io.github.vampireachao.stream.core.stream.Steam;
import io.github.vampireachao.stream.plugin.mybatisplus.Database;
import io.github.vampireachao.stream.plugin.mybatisplus.Many;
import lombok.val;
import org.apache.ibatis.type.SimpleTypeRegistry;
import org.springframework.beans.BeanUtils;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import static cn.hutool.core.text.CharSequenceUtil.genSetter;
import static java.util.Collections.emptyList;

/**
 * MpUtil
 *
 * @author VampireAchao
 * @since 2023/3/15
 */
@SuppressWarnings("unchecked")
public class MpUtil {

    public static <T,
            K extends Comparable<? super K> & Serializable,
            A,
            U extends Comparable<? super U> & Serializable,
            R> BaseDbBO<R> saveRelation(RelationBO<T, K, A, U, R> bo) {
        val mainList = bo.getMainList();
        val mainKeys = Steam.of(mainList).map(bo.getMainKey()).toList();
        val relationMainGetter = LambdaHelper.resolve(bo.getRelationMain());
        val relationMainSetter = getSetter(bo.getRelationMain());
        val relationAttachSetter = getSetter(bo.getRelationAttach());
        val relationClass = (Class<?>) relationMainGetter.getInstantiatedTypes()[0];

        val constructor = ((SerSupp<Constructor<?>>) relationClass::getConstructor).get();
        val constructorLambda = LambdaHelper.revert(Supplier.class, constructor);
        val nameRelationCompareSetterMap = Steam.of(bo.getRelationCompares())
                .map(c -> LambdaHelper.resolve((Serializable) c))
                .<String, BiConsumer<R, Object>>toMap(LambdaExecutable::getName, MpUtil::getSetter);
        val willInsertList = Steam.of(mainList).flatMap(vo -> Steam.of(bo.getAttachGetter().apply(vo)))
                .filter(bo.getAttachKey(), null)
                .toList();
        if (!willInsertList.isEmpty()) {
            if (bo.getInsertOnMissAttach()) {
                Database.saveFewSql(willInsertList);
            } else {
                throw new IllegalStateException("attach data missing");
            }
        }
        val relationsFromClient = Steam.of(mainList)
                .flatMap(vo -> Steam.of(bo.getAttachGetter().apply(vo))
                        .map(attach -> {
                            R relation = (R) constructorLambda.get();
                            relationMainSetter.accept(relation, bo.getMainKey().apply(vo));
                            relationAttachSetter.accept(relation, bo.getAttachKey().apply(attach));
                            Steam.of(bo.getAttachCompares()).forEach(c -> {
                                val executable = LambdaHelper.resolve((Serializable) c);
                                val setter = nameRelationCompareSetterMap.get(executable.getName());
                                setter.accept(relation, c.apply(attach));
                            });
                            return relation;
                        })
                ).toList();
        val relationMain = bo.getRelationMain();
        val relationsFromDb = Many.of(relationMain).in(mainKeys).query();
        if (relationsFromClient.isEmpty()) {
            bo.setWillDeleteList(relationsFromDb);
            return bo;
        }
        if (relationsFromDb.isEmpty()) {
            bo.setWillInsertList(relationsFromClient);
            bo.setDataList(relationsFromClient);
            return bo;
        }
        val mainIdRelationsMapFromDb = Steam.of(relationsFromDb)
                .group(relationMain);
        val mainIdRelationsMapFromClient = Steam.of(relationsFromClient)
                .group(relationMain);
        mainKeys.forEach(mainKey -> {
            val relationListFromDb = mainIdRelationsMapFromDb.getOrDefault(mainKey, emptyList());
            val relationListFromClient = mainIdRelationsMapFromClient.getOrDefault(mainKey, emptyList());
            if (relationListFromDb.isEmpty() && relationListFromClient.isEmpty()) {
                return;
            }
            if (relationListFromDb.isEmpty()) {
                bo.getWillInsertList().addAll(relationListFromClient);
                return;
            }
            if (relationListFromClient.isEmpty()) {
                bo.getWillDeleteList().addAll(relationListFromDb);
                return;
            }
            val attachKeyRelationMapFromClient = Steam.of(relationListFromClient)
                    .toMap(bo.getRelationAttach());
            val attachKeysFromClient = attachKeyRelationMapFromClient.keySet();
            val attachKeyRelationMapFromDb = Steam.of(relationListFromDb)
                    .toMap(bo.getRelationAttach());
            val attachKeysFromDb = attachKeyRelationMapFromDb.keySet();
            // insert
            Steam.of(attachKeysFromClient).filter(attachKey -> !attachKeysFromDb.contains(attachKey))
                    .map(attachKeyRelationMapFromClient::get)
                    .forEach(bo.getWillInsertList()::add);
            // remove
            Steam.of(attachKeysFromDb).filter(attachKey -> !attachKeysFromClient.contains(attachKey))
                    .map(attachKeyRelationMapFromDb::get)
                    .forEach(bo.getWillDeleteList()::add);
            // modify
            attachKeysFromDb.retainAll(attachKeysFromClient);
            attachKeysFromDb.forEach(attachKey -> {
                val relationFromDb = attachKeyRelationMapFromDb.get(attachKey);
                val relationFromClient = attachKeyRelationMapFromClient.get(attachKey);
                if (Objects.nonNull(relationFromDb) && Objects.nonNull(relationFromClient)) {
                    Steam.of(bo.getRelationCompares()).forEach(ac -> {
                        val value = ac.apply(relationFromClient);
                        if (!Objects.equals(value, ac.apply(relationFromDb))) {
                            val executable = LambdaHelper.resolve((Serializable) ac);
                            val setter = nameRelationCompareSetterMap.get(executable.getName());
                            setter.accept(relationFromDb, value);
                            if (!bo.getWillUpdateList().contains(relationFromDb)) {
                                bo.getWillUpdateList().add(relationFromDb);
                            }
                        }
                    });
                }
            });
        });
        bo.setAfterExecuted(b -> {
            val relationPrimaryKey = MpUtil.getGetter((Class<R>) relationClass,
                    TableInfoHelper.getTableInfo(relationClass).getKeyProperty());
            relationsFromDb.removeIf(bo.getWillDeleteList()::contains);
            relationsFromDb.addAll(bo.getWillInsertList());
            val idAttachMap = Steam.of(relationsFromDb).toMap(relationPrimaryKey);
            relationsFromClient.forEach(data -> {
                val pk = relationPrimaryKey.apply(data);
                idAttachMap.put(pk, data);
            });
            bo.setDataList(new ArrayList<>(idAttachMap.values()));
        });
        return bo;
    }

    public static <T,
            K extends Comparable<? super K> & Serializable,
            A,
            L extends Comparable<? super L> & Serializable>
    BaseDbBO<A> saveAttach(AttachBO<T, K, A> bo) {
        val mainList = bo.getMainList();

        val mainKeys = Steam.of(mainList).map(bo.getMainKey()).toList();
        val attachKeyExecutable = LambdaHelper.resolve(bo.getAttachKey());
        val attachKeySetter = getSetter(attachKeyExecutable);
        val compareGetterSetterMap = Steam.of(bo.getAttachCompares())
                .toMap(Function.identity(), MpUtil::getSetter);
        val attachListFromClient = Steam.of(mainList).flat(m -> Steam.of(bo.getAttachGetter().apply(m))
                        .peek(a -> attachKeySetter.accept(a, bo.getMainKey().apply(m))))
                .toList();
        val attachGetterExecutable = LambdaHelper.resolve(bo.getAttachGetter());
        val field = ReflectHelper.getField(attachGetterExecutable.getClazz(), BeanHelper.getPropertyName(attachGetterExecutable.getName()));
        Class<A> genericType = (Class<A>) ReflectHelper.getGenericTypes(field.getGenericType())[0];
        val attachListFromDb = Many.of(bo.getAttachKey()).in(mainKeys)
                .value(v -> {
                    val vo = ((SerSupp<A>) genericType::newInstance).get();
                    BeanUtils.copyProperties(v, vo);
                    return vo;
                })
                .query();
        val attachTableInfo = TableInfoHelper.getTableInfo(attachKeyExecutable.getClazz());
        val attachPrimaryKey = MpUtil.<A, L>getGetter((Class<A>) attachKeyExecutable.getClazz(),
                attachTableInfo.getKeyProperty());
        val attachKeyListFromDb = Steam.of(attachListFromDb).map(attachPrimaryKey).toList();
        if (attachKeyListFromDb.isEmpty()) {
            bo.setWillInsertList(attachListFromClient);
            bo.setDataList(attachListFromClient);
            return bo;
        }
        if (attachListFromClient.isEmpty()) {
            bo.setWillDeleteList(attachListFromDb);
            return bo;
        }
        val mainIdAttachesMapFromDb = Steam.of(attachListFromDb).group(bo.getAttachKey());
        val mainIdAttachesMapFromClient = Steam.of(attachListFromClient).group(bo.getAttachKey());
        mainKeys.forEach(mainKey -> {
            val attachesFromDb = mainIdAttachesMapFromDb.getOrDefault(mainKey, emptyList());
            val attachesFromClient = mainIdAttachesMapFromClient.getOrDefault(mainKey, emptyList());
            if (attachesFromDb.isEmpty() && attachesFromClient.isEmpty()) {
                return;
            }
            if (attachesFromDb.isEmpty()) {
                bo.getWillInsertList().addAll(attachesFromClient);
                return;
            }
            if (attachesFromClient.isEmpty()) {
                bo.getWillDeleteList().addAll(attachesFromDb);
                return;
            }
            val idAttachMapFromDb = Steam.of(attachesFromDb).toMap(attachPrimaryKey);
            // insert
            Steam.of(attachesFromClient)
                    .filter(attach -> {
                        val id = attachPrimaryKey.apply(attach);
                        return Objects.isNull(id) || !idAttachMapFromDb.containsKey(id);
                    })
                    .forEach(bo.getWillInsertList()::add);
            val idAttachMapFromClient = Steam.of(attachesFromClient)
                    .filter(attach -> {
                        val id = attachPrimaryKey.apply(attach);
                        return Objects.nonNull(id);
                    }).toMap(attachPrimaryKey);
            // remove
            val attachKeysFromDb = idAttachMapFromDb.keySet();
            Steam.of(attachKeysFromDb)
                    .filter(id -> !idAttachMapFromClient.containsKey(id))
                    .map(idAttachMapFromDb::get)
                    .forEach(bo.getWillDeleteList()::add);
            // modify
            attachKeysFromDb.retainAll(idAttachMapFromClient.keySet());
            attachKeysFromDb.forEach(attachKey -> {
                val attachFromDb = idAttachMapFromDb.get(attachKey);
                val attachFromClient = idAttachMapFromClient.get(attachKey);
                if (Objects.nonNull(attachFromDb) && Objects.nonNull(attachFromClient)) {
                    Steam.of(bo.getAttachCompares()).forEach(ac -> {
                        val value = ac.apply(attachFromClient);
                        if (!Objects.equals(value, ac.apply(attachFromDb))) {
                            val executable = LambdaHelper.resolve((Serializable) ac);
                            val setter = compareGetterSetterMap.get(ac);
                            val fieldType = ReflectHelper.getField(executable.getClazz(),
                                    BeanHelper.getPropertyName(executable.getName())).getType();
                            setter.accept(attachFromDb, value);
                            if (isComparable(fieldType) && (!bo.getWillUpdateList().contains(attachFromDb))) {
                                bo.getWillUpdateList().add(attachFromDb);

                            }
                        }
                    });
                }
            });
        });
        bo.setAfterExecuted(b -> {
            attachListFromDb.removeIf(bo.getWillDeleteList()::contains);
            attachListFromDb.addAll(bo.getWillInsertList());
            val idAttachMap = Steam.of(attachListFromDb).toMap(attachPrimaryKey);
            attachListFromClient.forEach(data -> {
                val primaryKey = attachPrimaryKey.apply(data);
                idAttachMap.put(primaryKey, data);
            });
            bo.setDataList(new ArrayList<>(idAttachMap.values()));
        });
        return bo;
    }

    public static <T, K extends Comparable<? super K> & Serializable, S> BaseDbBO<S> saveSub(SubBO<T, K, S> bo) {
        val subIds = Steam.of(bo.getMainList())
                .flat(data -> Steam.of(bo.getSubIdGetters()).map(f -> f.apply(data)))
                .nonNull().toList();
        val subClazz = (Class<S>) Steam.of(bo.getSubGetters())
                .findFirst()
                .map(LambdaHelper::resolve)
                .map(LambdaExecutable::getReturnType)
                .orElseThrow(() -> new IllegalStateException("sub class not found"));
        val primaryKeyGetter = MpUtil.<S, K>getGetter(subClazz,
                TableInfoHelper.getTableInfo(subClazz).getKeyProperty());
        val idSubMapFromDb = subIds.isEmpty() ? new HashMap<K, S>() :
                Steam.of(Database.listByIds(subIds, subClazz)).toMap(primaryKeyGetter);
        val subIdGetterSetterMap = Steam.of(bo.getSubIdGetters())
                .toMap(Function.identity(), MpUtil::getSetter);
        val comparesGetterSetterMap = Steam.of(bo.getSubCompares())
                .toMap(Function.identity(), MpUtil::getSetter);

        Steam.of(bo.getMainList()).forEach(data -> {
            Steam.of(bo.getSubIdGetters()).zip(bo.getSubGetters(),
                    (subIdGetter, subGetter) -> {
                        val subFromClient = subGetter.apply(data);
                        val subId = subIdGetter.apply(data);
                        if (Objects.isNull(subFromClient)) {
                            return null;
                        }
                        // insert
                        if (Objects.isNull(subId)) {
                            bo.getWillInsertList().add(subFromClient);
                            return subFromClient;
                        }
                        val subFromDb = idSubMapFromDb.get(subId);
                        if (Objects.isNull(subFromDb)) {
                            bo.getWillInsertList().add(subFromClient);
                            return subFromClient;
                        }
                        // update
                        Steam.of(bo.getSubCompares()).forEach(ac -> {
                            val value = ac.apply(subFromClient);
                            if (!Objects.equals(value, ac.apply(subFromDb))) {
                                val executable = LambdaHelper.resolve((Serializable) ac);
                                val setter = (BiConsumer<S, Object>) comparesGetterSetterMap.get(ac);
                                val fieldType = ReflectHelper.getField(executable.getClazz(),
                                        BeanHelper.getPropertyName(executable.getName())).getType();
                                setter.accept(subFromDb, value);
                                if (isComparable(fieldType) && (!bo.getWillUpdateList().contains(subFromDb))) {
                                    bo.getWillUpdateList().add(subFromDb);
                                }
                            }
                        });
                        return subFromClient;
                    }).toList();
        });
        bo.setAfterExecuted(b -> {
            val dataList = Steam.of(bo.getMainList()).flat(data ->
                    Steam.of(bo.getSubIdGetters()).zip(bo.getSubGetters(),
                            (subIdGetter, subGetter) -> {
                                S subFromClient = subGetter.apply(data);
                                if (Objects.nonNull(subFromClient)) {
                                    val primaryKey = primaryKeyGetter.apply(subFromClient);
                                    subIdGetterSetterMap.get(subIdGetter).accept(data, primaryKey);
                                }
                                return subFromClient;
                            }).nonNull().toList()).toList();
            bo.setDataList(dataList);
        });
        return bo;
    }


    public static <T, R> SFunction<T, R> getGetter(Class<T> clazz, String property) {
        return LambdaHelper.revert(
                SFunction.class,
                ReflectHelper.getMethod(
                        clazz,
                        StrUtil.genGetter(property)
                )
        );
    }

    public static <T, R> BiConsumer<T, R> getSetter(SFunction<T, R> getter) {
        return getSetter(LambdaHelper.resolve(getter));
    }

    public static <T, R> BiConsumer<T, R> getSetter(LambdaExecutable executable) {
        val setterName = genSetter(BeanHelper.getPropertyName(executable.getName()));
        val setter = Steam.of(ReflectHelper.getMethods(executable.getClazz()))
                .findFirst(m -> m.getName().equals(setterName))
                .orElse(null);
        return LambdaHelper.revert(BiConsumer.class, setter);
    }

    public static boolean isComparable(Class<?> returnType) {
        if (SimpleTypeRegistry.isSimpleType(returnType)) {
            return true;
        }
        if (returnType.isEnum()) {
            return true;
        }
        if (TemporalAccessor.class.isAssignableFrom(returnType)) {
            return true;
        }
        if (Date.class.isAssignableFrom(returnType)) {
            return true;
        }
        if (Collection.class.isAssignableFrom(returnType)) {
            return false;
        }
        if (Map.class.isAssignableFrom(returnType)) {
            return false;
        }
        if (BeanUtil.isBean(returnType)) {
            return false;
        }
        return false;
    }
}
